1 /** 2 * This module implements $(D TestSuite), an aggregator for $(D TestCase) 3 * objects to run all tests. 4 */ 5 6 module unit_threaded.testsuite; 7 8 import unit_threaded.from; 9 10 /* 11 * taskPool.amap only works with public functions, not closures. 12 */ 13 auto runTest(from!"unit_threaded.testcase".TestCase test) { 14 return test(); 15 } 16 17 /** 18 * Responsible for running tests and printing output. 19 */ 20 struct TestSuite { 21 import unit_threaded.io : Output; 22 import unit_threaded.options : Options; 23 import unit_threaded.reflection : TestData; 24 import unit_threaded.testcase : TestCase; 25 import std.datetime : Duration; 26 27 static if (__VERSION__ >= 2077) 28 import std.datetime.stopwatch : StopWatch; 29 else 30 import std.datetime : StopWatch; 31 32 this(in Options options, in TestData[] testData) { 33 import unit_threaded.io : WriterThread; 34 35 this(options, testData, WriterThread.get); 36 } 37 38 /** 39 * Params: 40 * options = The options to run tests with. 41 * testData = The information about the tests to run. 42 * output = Where to send text output. 43 */ 44 this(in Options options, in TestData[] testData, Output output) { 45 import unit_threaded.factory : createTestCases; 46 47 _options = options; 48 _testData = testData; 49 _output = output; 50 _testCases = createTestCases(testData, options.testsToRun); 51 } 52 53 /** 54 * Runs all test cases. 55 * Returns: true if no test failed, false otherwise. 56 */ 57 bool run() { 58 59 import unit_threaded.io : writelnRed, writeln, writeRed, write, 60 writeYellow, writelnGreen; 61 import std.algorithm : filter, count; 62 import std.conv : text; 63 64 if (!_testCases.length) { 65 _output.writelnRed("Error! No tests to run for args: "); 66 _output.writeln(_options.testsToRun); 67 return false; 68 } 69 70 immutable elapsed = doRun(); 71 72 if (!numTestsRun) { 73 _output.writeln("Did not run any tests!!!"); 74 return false; 75 } 76 77 _output.writeln("\nTime taken: ", elapsed); 78 _output.write(numTestsRun, " test(s) run, "); 79 const failuresStr = text(_failures.length, " failed"); 80 if (_failures.length) { 81 _output.writeRed(failuresStr); 82 } else { 83 _output.write(failuresStr); 84 } 85 86 ulong numTestsWithAttr(string attr)() { 87 return _testData.filter!(a => mixin("a. " ~ attr)).count; 88 } 89 90 void printHidden() { 91 const num = numTestsWithAttr!"hidden"; 92 if (!num) 93 return; 94 _output.write(", "); 95 _output.writeYellow(num, " ", "hidden"); 96 } 97 98 void printShouldFail() { 99 const total = numTestsWithAttr!"shouldFail"; 100 ulong num = total; 101 102 foreach (f; _failures) { 103 const data = _testData.filter!(a => a.getPath == f).front; 104 if (data.shouldFail) 105 --num; 106 } 107 108 if (!total) 109 return; 110 _output.write(", "); 111 _output.writeYellow(num, "/", total, " ", "failing as expected"); 112 } 113 114 printHidden(); 115 printShouldFail(); 116 117 _output.writeln(".\n"); 118 119 if (_options.random) 120 _output.writeln("Tests were run in random order. To repeat this run, use --seed ", 121 _options.seed, "\n"); 122 123 if (_failures.length) { 124 _output.writelnRed("Tests failed!\n"); 125 return false; //oops 126 } 127 128 _output.writelnGreen("OK!\n"); 129 130 return true; 131 } 132 133 private: 134 135 const(Options) _options; 136 const(TestData)[] _testData; 137 TestCase[] _testCases; 138 string[] _failures; 139 StopWatch _stopWatch; 140 Output _output; 141 142 /** 143 * Runs the tests with the given options. 144 * Returns: how long it took to run. 145 */ 146 Duration doRun() { 147 148 import std.algorithm : reduce; 149 import std.parallelism : taskPool; 150 151 auto tests = getTests(); 152 153 if (_options.showChrono) 154 foreach (test; tests) 155 test.showChrono; 156 157 _stopWatch.start(); 158 159 if (_options.multiThreaded) { 160 _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests)); 161 } else { 162 foreach (test; tests) { 163 _failures ~= test(); 164 } 165 } 166 167 handleFailures(); 168 169 _stopWatch.stop(); 170 return cast(Duration) _stopWatch.peek(); 171 } 172 173 auto getTests() { 174 import unit_threaded.io : writeln; 175 176 auto tests = _testCases.dup; 177 if (_options.random) { 178 import std.random; 179 180 auto generator = Random(_options.seed); 181 tests.randomShuffle(generator); 182 _output.writeln("Running tests in random order. ", 183 "To repeat this run, use --seed ", _options.seed); 184 } 185 return tests; 186 } 187 188 void handleFailures() { 189 import unit_threaded.io : writeln, writeRed, write; 190 import std.array : empty; 191 import std.algorithm : canFind; 192 193 if (!_failures.empty) 194 _output.writeln(""); 195 foreach (failure; _failures) { 196 _output.write("Test ", (failure.canFind(" ") ? `'` ~ failure ~ `'` : failure), " "); 197 _output.writeRed("failed"); 198 _output.writeln("."); 199 } 200 if (!_failures.empty) 201 _output.writeln(""); 202 } 203 204 @property ulong numTestsRun() @trusted const { 205 import std.algorithm : map, reduce; 206 207 return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b); 208 } 209 } 210 211 /** 212 * Replace the D runtime's normal unittest block tester. If this is not done, 213 * the tests will run twice. 214 */ 215 void replaceModuleUnitTester() { 216 import core.runtime : Runtime; 217 218 Runtime.moduleUnitTester = &moduleUnitTester; 219 } 220 221 version (unitThreadedLight) { 222 223 shared static this() { 224 import std.algorithm : canFind; 225 import std.parallelism : parallel; 226 import core.runtime : Runtime; 227 228 Runtime.moduleUnitTester = () { 229 230 // ModuleInfo has opApply, can't use parallel on that so we collect 231 // all the modules with unit tests first 232 ModuleInfo*[] modules; 233 foreach (module_; ModuleInfo) { 234 if (module_ && module_.unitTest) 235 modules ~= module_; 236 } 237 238 version (unitUnthreaded) 239 enum singleThreaded = true; 240 else 241 const singleThreaded = Runtime.args.canFind("-s") 242 || Runtime.args.canFind("--single"); 243 244 if (singleThreaded) 245 foreach (module_; modules) 246 module_.unitTest()(); 247 else 248 foreach (module_; modules.parallel) 249 module_.unitTest()(); 250 251 return true; 252 }; 253 } 254 255 } else { 256 shared static this() { 257 replaceModuleUnitTester; 258 } 259 } 260 261 /** 262 * Replacement for the usual unittest runner. Since unit_threaded 263 * runs the tests itself, the moduleUnitTester doesn't really have to do anything. 264 */ 265 private bool moduleUnitTester() { 266 //this is so unit-threaded's own tests run 267 import std.algorithm : startsWith; 268 269 foreach (module_; ModuleInfo) { 270 if (module_ && module_.unitTest && module_.name.startsWith("unit_threaded") 271 && // we want to run the "normal" unit tests 272 //!module_.name.startsWith("unit_threaded.property") && // left here for fast iteration when developing 273 !module_.name.startsWith("unit_threaded.ut.modules")) { //but not the ones from the test modules 274 version (testing_unit_threaded) { 275 import std.stdio : writeln; 276 277 writeln("Running unit-threaded UT for module " ~ module_.name); 278 } 279 module_.unitTest()(); 280 281 } 282 } 283 284 return true; 285 }